package org.budo.accesslog.interceptor;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;

import org.aopalliance.intercept.MethodInvocation;
import org.budo.accesslog.annotation.Accesslog;
import org.budo.accesslog.entity.AccesslogEntity;
import org.budo.accesslog.entity.builder.AccesslogEntityBuilder;
import org.budo.accesslog.entity.builder.BudoAccesslogEntityBuilder;
import org.budo.accesslog.exception.AccesslogRuntimeException;
import org.budo.accesslog.repository.AccesslogRepository;
import org.budo.accesslog.repository.impl.BufferedJdbcAccesslogRepository;
import org.budo.dubbo.protocol.http.spring.web.BudoHandlerMethod;
import org.budo.dubbo.protocol.http.util.BudoReflectionUtil;
import org.budo.support.lang.util.ReflectUtil;
import org.budo.support.lang.util.StringUtil;
import org.budo.support.mvcs.Mvcs;
import org.budo.support.spring.util.SpringUtil;
import org.springframework.web.method.HandlerMethod;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

/**
 * @author lmw
 */
@ToString
@Slf4j
@Getter
@Setter
public abstract class AbstractAccesslogInterceptor {
    private static final Boolean HAS_BUDO_HANDLER_METHOD_CLASS = ReflectUtil.hasClass("org.budo.dubbo.protocol.http.spring.web.BudoHandlerMethod");

    private static final String BEFORE_AT = "BEFORE_AT";

    private AccesslogRepository repositoryService;

    private String repositoryServiceBeanName;

    /**
     * 为 true 表示，仅处理有注解的，没注解就不处理
     */
    private Boolean annotation = false;

    public void setDataSource(DataSource dataSource) {
        if (null == dataSource) {
            log.error("#42 dataSource=" + dataSource + ", this=" + this);
            return;
        }

        AccesslogRepository repositoryService = new BufferedJdbcAccesslogRepository(dataSource);
        this.setRepositoryService(repositoryService);
    }

    protected void beforeAccess() {
        if (null == Mvcs.getRequest()) {
            return;
        }

        // 先读一遍 后读 可能发生 IOException: Stream closed
        // Mvcs.getRequestBody(); // 不能先读 你读了 别人读就会报错
        // Mvcs.getSession();

        long start = System.currentTimeMillis();
        Mvcs.setRequestAttribute(BEFORE_AT, start);
    }

    /**
     * 需要注解但没注解
     */
    private Boolean doAccesslog(Object handler) {
        // 为 true 表示，仅处理有注解的，没注解就不处理；false 或空 表示 全都处理
        if (null == this.getAnnotation() || !this.getAnnotation()) {
            return true; // 全局不需要
        }

        // 以上 getAnnotation 为 false 全都处理

        // 以下 getAnnotation 为 true
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Accesslog annotation = handlerMethod.getMethodAnnotation(Accesslog.class);
            return null != annotation; // 有注解需要
        }

        if (handler instanceof BudoHandlerMethod) {
            BudoHandlerMethod budoHandlerMethod = (BudoHandlerMethod) handler;
            Accesslog annotation = BudoReflectionUtil.getMethodAnnotationCached_1(budoHandlerMethod.getInterfaceName(), //
                    budoHandlerMethod.getMethodName(), //
                    budoHandlerMethod.getParameterTypes(), //
                    Accesslog.class);

            return null != annotation; // 有注解 需要
        }

        log.error("#95 handler=" + handler + ", requestURI=" + Mvcs.getRequestURI());
        return false; // 需要
    }

    protected void afterAccess() {
        if (null == Mvcs.getRequest()) {
            return;
        }

        Object handler = Mvcs.getHandler();
        if (!this.logThisHandler(handler)) { // 跳过
            log.debug("#45 afterAccess, handler={}", handler);
            return;
        }

        // 需要注解，但是没有注解
        boolean doAccesslog = this.doAccesslog(handler);
        if (!doAccesslog) {
            return;
        }

        Long start = (Long) Mvcs.getRequestAttribute(BEFORE_AT);
        if (null == start) {
            return;
        }

        long end = System.currentTimeMillis();

        int timeCost = (int) (end - start);

        AccesslogEntity accesslogEntity = this.newEntity(Mvcs.getRequest(), Mvcs.getError(), timeCost);

        this.saveAccesslog(accesslogEntity);
    }

    protected Boolean logThisHandler(Object handler) {
        if (null == handler) {
            return false;
        }

        if (handler instanceof HandlerMethod) {
            return true;
        }

        if (HAS_BUDO_HANDLER_METHOD_CLASS && handler instanceof BudoHandlerMethod) {
            return true;
        }

        return false;
    }

    protected void saveAccesslog(AccesslogEntity accesslogEntity) {
        AccesslogRepository repositoryService = this.repositoryService();
        if (null == repositoryService || null == accesslogEntity) {
            log.error("#132 saveAccesslog return, repositoryService = " + repositoryService + ", accesslogEntity=" + accesslogEntity);
            return;
        }

        try {
            repositoryService.save(accesslogEntity);
        } catch (Throwable e) {
            log.error("#139 saveAccesslog error, e=" + e, e);
        }
    }

    private AccesslogRepository repositoryService() {
        if (null != this.getRepositoryService()) {
            return this.getRepositoryService();
        }

        if (!StringUtil.isNullOrEmpty(this.getRepositoryServiceBeanName())) {
            AccesslogRepository repositoryService = (AccesslogRepository) SpringUtil.getBean(SpringUtil.getApplicationContext(), this.getRepositoryServiceBeanName(), false, true);
            log.info("#115 repositoryService=" + repositoryService + ", this=" + this);

            this.setRepositoryService(repositoryService);
            return repositoryService;
        }

        log.error("#116 repositoryService and repositoryServiceBeanName are null, this=" + this);
        return null;
    }

    protected void throwError(Throwable error, Object request) throws IOException, ServletException {
        if (log.isDebugEnabled()) {
            log.debug("#168 throwError, error=" + error + ", request=" + this.requestToString(request), error);
        }

        if (null == error) {
            // 为什么 error 为空呢
            // #163 throwError, error =null, request=ReflectiveMethodInvocation: public
            // org.budo.warehouse.logic.api.DataMessage
            // org.budo.warehouse.logic.filter.EventFilterLogicImpl.filterDataMessage
            // (org.budo.warehouse.service.entity.Pipeline,org.budo.warehouse.logic.api.DataMessage);
            // target is of class [org.budo.warehouse.logic.filter.EventFilterLogicImpl]
            // log.error("#163 throwError, error=" + error + ", request=" + request, error);
            return;
        }

        if (error instanceof IOException) {
            throw (IOException) error;
        }

        if (error instanceof ServletException) {
            throw (ServletException) error;
        }

        throw new AccesslogRuntimeException(error);
    }

    private String requestToString(Object request) {
        if (null == request) {
            return null;
        }

        if (request instanceof HttpServletRequest) {
            ServletRequest req = (ServletRequest) request;
            return "uri=" + Mvcs.getRequestURI(req) + ", headers=" + Mvcs.getRequestHeaderMap(req);
        }

        if (request instanceof MethodInvocation) {
            MethodInvocation invocation = (MethodInvocation) request;
            return invocation.getMethod() + "";
        }

        return null;
    }

    protected AccesslogEntity newEntity(ServletRequest request, Throwable error, int timeCost) {
        AccesslogEntityBuilder accesslogEntityBuilder = SpringUtil.getBeanCached(AccesslogEntityBuilder.class);
        if (null == accesslogEntityBuilder) {
            accesslogEntityBuilder = BudoAccesslogEntityBuilder.INSTANCE;
        }

        AccesslogEntity accesslogEntity = accesslogEntityBuilder.buildAccesslogEntity((HttpServletRequest) request);
        accesslogEntity.setTimeCost(timeCost);
        accesslogEntity.setError(null == error ? null : error.toString());
        return accesslogEntity;
    }
}